/*
 * Copyright 2007, 2008 Tiberiumlabs
 *
 * This file is part of Tiberiumlabs Shards.
 *
 * Tiberiumlabs Shards is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Tiberiumlabs Shards is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.tiberiumlabs.swing;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.net.URI;
import java.util.ResourceBundle;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import javax.swing.border.EtchedBorder;

/**
 * Component for displaying exceptions to user.<br/>
 * <p>
 * For now, we have only english, russian and ukrainian languages. If you
 * know more languages - we will gladly insert it to our resources.<br/>
 * <p>
 * Constructor of this class does all the job. Exception dialog
 * will be created and placed to the screen center.
 *
 * @author <a href="paranoid.tiberiumlabs@gmail.com">Paranoid</a>
 */
public class ExceptionDialog extends JDialog {

    private static final ResourceBundle bundle = ResourceBundle.getBundle("org/tiberiumlabs/swing/exceptions");
    private static final String br = "<br>";
    private static final String nbsp = "&nbsp;";

    private Exception exception = null;
    private String stackTrace = null;
    private String reportUrl = null;
    private String reportMail = null;
    private boolean notVisible = false;

    private CloseAction closeAction = new CloseAction();
    private JButton closeButton = new JButton(closeAction);

    /**
     *
     * @param owner the owner JFrame from which the dialog is displayed or null if this dialog has no owner
     * @param modal specifies whether dialog blocks user input to other top-level windows when shown.
     * @param exception the Exception class to display
     */
    public ExceptionDialog(JFrame owner, boolean modal, Exception exception) {
        super(owner, bundle.getString("dialog.title"), modal);
        this.exception = exception;

        createAndShow();
    }

    /**
     *
     * @param owner the owner JDialog from which the dialog is displayed or null if this dialog has no owner
     * @param modal specifies whether dialog blocks user input to other top-level windows when shown.
     * @param exception the Exception class to display
     */
    public ExceptionDialog(JDialog owner, boolean modal, Exception exception) {
        super(owner, bundle.getString("dialog.title"), modal);
        this.exception = exception;

        createAndShow();
    }

    /**
     *
     * @param owner the owner JFrame from which the dialog is displayed or null if this dialog has no owner
     * @param modal specifies whether dialog blocks user input to other top-level windows when shown.
     * @param exception the Exception class to display
     * @param reportUrl link to web page for bug reporting. If <code>null</code> - report button will not be
     * shown. Report button will try to use default system browser with this link
     * @param reportMail mail address for report sending. If <code>null</code> - mail button will not be
     * created. Mail button will try to open default system mail client and compose new email with
     * <code>reportMail</code> as recipient.
     */
    public ExceptionDialog(JFrame owner, boolean modal, Exception exception, String reportUrl, String reportMail) {
        super(owner, bundle.getString("dialog.title"), modal);
        this.exception = exception;
        this.reportMail = reportMail;
        this.reportUrl = reportUrl;

        createAndShow();
    }

    /**
     *
     * @param owner the owner JDialog from which the dialog is displayed or null if this dialog has no owner
     * @param modal specifies whether dialog blocks user input to other top-level windows when shown.
     * @param exception the Exception class to display
     * @param reportUrl link to web page for bug reporting. If <code>null</code> - report button will not be
     * shown. Report button will try to use default system browser with this link
     * @param reportMail mail address for report sending. If <code>null</code> - mail button will not be
     * created. Mail button will try to open default system mail client and compose new email with
     * <code>reportMail</code> as recipient.
     */
    public ExceptionDialog(JDialog owner, boolean modal, Exception exception, String reportUrl, String reportMail) {
        super(owner, bundle.getString("dialog.title"), modal);
        this.exception = exception;
        this.reportMail = reportMail;
        this.reportUrl = reportUrl;

        createAndShow();
    }

    /**
     *
     * @param owner the owner JFrame from which the dialog is displayed or null if this dialog has no owner
     * @param modal specifies whether dialog blocks user input to other top-level windows when shown.
     * @param exception the Exception class to display
     * @param reportUrl link to web page for bug reporting. If <code>null</code> - report button will not be
     * shown. Report button will try to use default system browser with this link
     * @param reportMail mail address for report sending. If <code>null</code> - mail button will not be
     * created. Mail button will try to open default system mail client and compose new email with
     * <code>reportMail</code> as recipient.
     * @param notVisible if <code>true</code> - dialog will not be visible after creation, you will need to
     * make it visible by <code>setVisible(true)</code> method. false by default.
     */
    public ExceptionDialog(JFrame owner, boolean modal, Exception exception, String reportUrl, String reportMail, boolean notVisible) {
        super(owner, bundle.getString("dialog.title"), modal);
        this.exception = exception;
        this.reportMail = reportMail;
        this.reportUrl = reportUrl;
        this.notVisible = notVisible;

        createAndShow();
    }

    /**
     *
     * @param owner the owner JDialog from which the dialog is displayed or null if this dialog has no owner
     * @param modal specifies whether dialog blocks user input to other top-level windows when shown.
     * @param exception the Exception class to display
     * @param reportUrl link to web page for bug reporting. If <code>null</code> - report button will not be
     * shown. Report button will try to use default system browser with this link
     * @param reportMail mail address for report sending. If <code>null</code> - mail button will not be
     * created. Mail button will try to open default system mail client and compose new email with
     * <code>reportMail</code> as recipient.
     * @param notVisible if <code>true</code> - dialog will not be visible after creation, you will need to
     * make it visible by <code>setVisible(true)</code> method. false by default.
     */
    public ExceptionDialog(JDialog owner, boolean modal, Exception exception, String reportUrl, String reportMail, boolean notVisible) {
        super(owner, bundle.getString("dialog.title"), modal);
        this.exception = exception;
        this.reportMail = reportMail;
        this.reportUrl = reportUrl;
        this.notVisible = notVisible;

        createAndShow();
    }

    private void createAndShow() {
        JPanel contentPane = new JPanel(new BorderLayout());

        contentPane.add(getIconPanel(), BorderLayout.WEST);
        contentPane.add(getTextPanel(), BorderLayout.CENTER);
        contentPane.add(getButtonsPanel(), BorderLayout.SOUTH);

        setContentPane(contentPane);
        getRootPane().setDefaultButton(closeButton);

        pack();

        Dimension size = getSize();
        int needWidth = 640;
        int needHeight = 480;
        if (size.width <= needWidth) {
            needWidth = size.width;
        }
        if (size.height <= needHeight) {
            needHeight = size.height;
        }
        setSize(new Dimension(needWidth, needHeight));

        setLocation(DesktopToolkit.getWindowLocation(getSize()));

        if (notVisible) {
            return;
        }

        setVisible(true);
        closeButton.requestFocus();
    }

    private JComponent getIconPanel() {
        JPanel panel = new JPanel();
        JLabel icon = new JLabel(new ImageIcon(ExceptionDialog.class.getResource("error.png")));
        icon.setBorder(BorderFactory.createEmptyBorder(4, 4, 8, 8));
        panel.add(icon);
        return panel;
    }

    private JComponent getTextPanel() {

        // <editor-fold defaultstate="collapsed" desc=" format stack trace ">

        StringBuilder buffer = new StringBuilder();
        buffer.append("<table><tbody><tr><td>");
        buffer.append(bundle.getString("exception.class"));
        buffer.append(":</td><td><b>");
        buffer.append(exception.getClass().getName());
        buffer.append("</b></td></tr><tr><td>");
        buffer.append(bundle.getString("exception.message"));
        buffer.append(":&nbsp;</td><td><b>");
        buffer.append(exception.getLocalizedMessage());
        buffer.append("</b></td></tr></tbody></table><br><hr><br>");
        buffer.append(getStackTrace());

        // </editor-fold>

        JEditorPane textArea = new JEditorPane("text/html", buffer.toString());
        textArea.setEditable(false);
        JScrollPane scrollPane = new JScrollPane(textArea);
        scrollPane.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(10, 0, 10, 10), scrollPane.getBorder()));

        // <editor-fold defaultstate="collapsed" desc=" key bindings ">
        InputMap inputMap = textArea.getInputMap(JComponent.WHEN_FOCUSED);
        ActionMap actionMap = textArea.getActionMap();
        String closeActionCommandName = "exceptiondialog:close";
        actionMap.put(closeActionCommandName, closeAction);
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), closeActionCommandName);
        // </editor-fold>

        return scrollPane;
    }

    private JComponent getButtonsPanel() {
        Box box = Box.createHorizontalBox();

        if (reportUrl != null) {
            box.add(new JButton(new ReportAction()));
        }
        if (reportMail != null) {
            if (reportUrl != null) {
                box.add(Box.createHorizontalStrut(6));
            }
            box.add(new JButton(new MailAction()));
        }
        box.add(Box.createHorizontalGlue());
        box.add(Box.createHorizontalStrut(12));
        box.add(closeButton);

        box.setBorder(BorderFactory.createCompoundBorder(new EtchedBorder() {

            @Override
            public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
                g.translate(x, y);

                g.setColor(getShadowColor(c));
                g.drawLine(0, 0, width, 0);

                g.setColor(getHighlightColor(c));
                g.drawLine(0, 1, width, 1);

                g.translate(-x, -y);
            }
        }, BorderFactory.createEmptyBorder(8, 8, 8, 8)));

        return box;
    }

    private String getStackTrace() {
        if (stackTrace == null) {
            StringBuilder sb = new StringBuilder();
            sb.append(exception).append(br);
            String at = nbsp + nbsp + nbsp + nbsp + "at ";
            StackTraceElement[] trace = exception.getStackTrace();
            if (trace != null && trace.length > 0) {
                for (int i = 0; i < trace.length; i++) {
                    sb.append(at);
                    sb.append(trace[i]);
                    sb.append(br);
                }
            }
            Throwable cause = exception.getCause();
            while (cause != null) {
                StackTraceElement[] causedTrace = trace;
                trace = cause.getStackTrace();
                int m = trace.length - 1;
                int n = causedTrace.length - 1;
                while (m >= 0 && n >= 0 && trace[m].equals(causedTrace[n])) {
                    m--;
                    n--;
                }
                int framesInCommon = trace.length - 1 - m;
                sb.append("Caused by: ");
                sb.append(cause).append(br);
                for (int i = 0; i <= m; i++) {
                    sb.append(at).append(trace[i]).append(br);
                }
                if (framesInCommon != 0) {
                    sb.append(nbsp).append(nbsp).append(nbsp).append(nbsp);
                    sb.append("...").append(framesInCommon).append(" more").append(br);
                }
                cause = cause.getCause();
            }
            stackTrace = sb.toString();
        }
        return stackTrace;
    }

    private boolean executeCommand(String command) {
        String application = System.getenv("DESKTOP_LAUNCH");
        if (application != null && !application.isEmpty()) {
            ProcessBuilder builder = new ProcessBuilder(application, command);
            try {
                builder.start();
                return true;
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        return false;
    }

    private class CloseAction extends AbstractAction {

        public CloseAction() {
            putValue(NAME, bundle.getString("dialog.close.name"));
            putValue(SHORT_DESCRIPTION, bundle.getString("dialog.close.short"));
            putValue(LONG_DESCRIPTION, bundle.getString("dialog.close.long"));
        }

        public void actionPerformed(ActionEvent e) {
            dispose();
        }
    }

    private class ReportAction extends AbstractAction {

        public ReportAction() {
            putValue(NAME, bundle.getString("button.report.name"));
            putValue(SHORT_DESCRIPTION, bundle.getString("button.report.short"));
            putValue(LONG_DESCRIPTION, bundle.getString("button.report.long"));
        }

        public void actionPerformed(ActionEvent e) {
            String command = reportUrl;

            if (executeCommand(command)) {
                return;
            }

            if (Desktop.isDesktopSupported()) {
                try {
                    Desktop.getDesktop().browse(new URI(command));
                    return;
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }

            ProcessBuilder builder = new ProcessBuilder(command);
            try {
                builder.start();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    private class MailAction extends AbstractAction {

        public MailAction() {
            putValue(NAME, bundle.getString("button.mail.name"));
            putValue(SHORT_DESCRIPTION, bundle.getString("button.mail.short"));
            putValue(LONG_DESCRIPTION, bundle.getString("button.mail.long"));
        }

        public void actionPerformed(ActionEvent e) {

            String command = "mailto:" + reportMail;

            if (executeCommand(command)) {
                return;
            }

            if (Desktop.isDesktopSupported()) {
                try {
                    Desktop.getDesktop().mail(new URI(command));
                    return;
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }

            ProcessBuilder builder = new ProcessBuilder(command);
            try {
                builder.start();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

}
